<?php
/**
 * @Author: feidao@ibantang.com, 2020, all rights reserved
 * @Date: 2020/2/27
 * @Time: 8:25 PM
 * @Brief:
 */

namespace Core\Common\Base;

use Core\Common\Route\Request;

// http://wiki.17gwx.com/display/RD/41-Jaguar
class Jaguar
{
    private static $pool = [];
    private $name = ''; // jaguar 配置名
    private $client = null;

    private function __construct($name, $client)
    {
        $this->name = $name;
        $this->client = $client;
    }

    /**
     * 获取连接在池子里的名称
     * @param $name
     * @return string
     */
    private static function getPoolKeyByName($name)
    {
        $inMars = Request::isRunningInMars();
        $poolKey = $name;
        if ($inMars) {
            $poolKey .= "_mars";
        }
        return $poolKey;
    }

    /**
     * 获取一个 jaguar 连接实例
     * @param $name
     * @return bool|mixed
     */
    public static function getInstance($name)
    {
        if (Request::isRunningInMars()) {
            $name = 'mars_mock';
        }
        $poolKey = self::getPoolKeyByName($name);
        if (isset(self::$pool[$poolKey])) {
            return self::$pool[$poolKey];
        }
        $client = self::connect($name);
        if (empty($client)) {
            return false;
        }

        self::$pool[$poolKey] = new self($name, $client);
        return self::$pool[$poolKey];
    }

    private static function connect($name)
    {
        $config = Config::getJaguar($name);
        if (empty($config)) {
            return false;
        }

        $timeout = $config['timeout'] ?? 0;
        $host = $config['host'];
        $port = $config['port'] ?? 6379;

        $redis = new \Redis();
        $redis->pconnect($host, $port, $timeout, $name);
        return $redis;
    }

    /**
     * 重新连接redis
     */
    private function reconnect()
    {
        $poolKey = self::getPoolKeyByName($this->name);
        unset(self::$pool[$poolKey]);
        $this->client = self::connect($this->name);
    }

    /**
     * 执行命令
     * @param $name
     * @param $arguments
     * @return mixed
     */
    private function retry($name, $arguments)
    {
        $t1 = microtime(true);
        $ret = $this->client->$name(...$arguments);
        $t2 = microtime(true);
        $detail = [
            'connection' => self::getPoolKeyByName($this->name),
            'cmd' => $name,
            'query' => [$name, $arguments],
            'ms' => ($t2 - $t1) * 1000,
        ];
        $this->log($detail);
        return $ret;
    }

    //////////// 支持的命令列表 ///////////// {{{
    /// https://raw.githubusercontent.com/twitter/twemproxy/master/notes/redis.md
    /// KEY commands
    public function DEL(...$key)
    {
        return $this->execCommand("DEL", $key);
    }

    public function EXISTS($key)
    {
        return $this->execCommand("EXISTS", [$key,]);
    }

    public function EXPIRE($key, $seconds)
    {
        return $this->execCommand("EXPIRE", [$key, $seconds]);
    }

    public function TTL($key)
    {
        return $this->execCommand("TTL", [$key]);
    }

    /// String commands
    public function APPEND($key, $value)
    {
        return $this->execCommand("APPEND", [$key, $value]);
    }

    public function DECR($key)
    {
        return $this->execCommand("DECR", [$key]);
    }

    public function DECRBY($key, $decrement)
    {
        return $this->execCommand("DECRBY", [$key, $decrement]);
    }

    public function GET($key)
    {
        return $this->execCommand("GET", [$key,]);
    }

    public function GETSET($key, $value)
    {
        return $this->execCommand("GETSET", [$key, $value]);
    }

    public function INCR($key)
    {
        return $this->execCommand("INCR", [$key]);
    }

    public function INCRBY($key, $increment)
    {
        return $this->execCommand("INCRBY", [$key, $increment]);
    }

//    public function INCRBYFLOAT($key, $increment)
//    {
//        return $this->execCommand("INCRBYFLOAT", [$key, $increment]);
//    }

//    /**
//     * MSET support is not Atomic
//     * @return mixed|null
//     */
//    public function MSET($key, $value, ...$others)
//    {
//        array_unshift($others, $key, $value);
//        return $this->execCommand("MSET", $others);
//    }
//
//    public function MGET(...$key)
//    {
//        return $this->execCommand("MGET", $key);
//    }

    // SET key value [EX seconds] [PX milliseconds] [NX|XX]
    public function SET($key, $value, ...$args)
    {
        array_unshift($args, $key, $value);
        return $this->execCommand("SET", $args);
    }

    public function SETEX($key, $seconds, $value)
    {
        return $this->execCommand("SETEX", [$key, $seconds, $value]);
    }

    public function SETNX($key, $value)
    {
        return $this->execCommand("SETNX", [$key, $value]);
    }

    public function STRLEN($key)
    {
        return $this->execCommand("STRLEN", [$key]);
    }

    /// Hashes ///
    public function HDEL($key, ...$field)
    {
        array_unshift($field, $key);
        return $this->execCommand("HDEL", $field);
    }

    public function HEXISTS($key, $field)
    {
        return $this->execCommand("HEXISTS", [$key, $field]);
    }

    public function HGET($key, $field)
    {
        return $this->execCommand("HGET", [$key, $field]);
    }

    public function HGETALL($key)
    {
        return $this->execCommand("HGETALL", [$key]);
    }

    public function HINCRBY($key, $field, $increment)
    {
        return $this->execCommand("HINCRBY", [$key, $field, $increment]);
    }

//    public function HINCRBYFLOAT($key, $field, $increment)
//    {
//        return $this->execCommand("HINCRBYFLOAT", [$key, $field, $increment]);
//    }

    public function HKEYS($key)
    {
        return $this->execCommand("HKEYS", [$key]);
    }

    public function HLEN($key)
    {
        return $this->execCommand("HLEN", [$key]);
    }

    public function HMGET($key, ...$field)
    {
        array_unshift($field, $key);
        return $this->execCommand("HMGET", $field);
    }

    public function HMSET($key, $values)
    {
        return $this->execCommand("HMSET", [$key, $values,]);
    }

    public function HSET($key, $field, $value)
    {
        return $this->execCommand("HSET", [$key, $field, $value]);
    }

    public function HSETNX($key, $field, $value)
    {
        return $this->execCommand("HSETNX", [$key, $field, $value]);
    }

    public function HVALS($key)
    {
        return $this->execCommand("HVALS", [$key]);
    }

    /// Lists ///
    public function LINDEX($key, $index)
    {
        return $this->execCommand("LINDEX", [$key, $index]);
    }

//    /**
//     * LINSERT key BEFORE|AFTER pivot value
//     */
//    public function LINSERT($key, ...$other)
//    {
//        array_unshift($other, $key);
//        return $this->execCommand("LINSERT", $other);
//    }

    public function LLEN($key)
    {
        return $this->execCommand("LLEN", [$key]);
    }

    public function LPOP($key)
    {
        return $this->execCommand("LPOP", [$key]);
    }

    public function LPUSH($key, ...$values)
    {
        array_unshift($values, $key);
        return $this->execCommand("LPUSH", $values);
    }

    public function LPUSHX($key, $value)
    {
        return $this->execCommand("LPUSHX", [$key, $value]);
    }

    public function LRANGE($key, $start, $stop)
    {
        return $this->execCommand("LRANGE", [$key, $start, $stop]);
    }

    public function RPOP($key)
    {
        return $this->execCommand("RPOP", [$key]);
    }

    public function RPUSH($key, $value)
    {
        return $this->execCommand("RPUSH", [$key, $value]);
    }

    public function RPUSHX($key, $value)
    {
        return $this->execCommand("RPUSHX", [$key, $value]);
    }

    /// set ///
    public function SADD($key, ...$member)
    {
        array_unshift($member, $key);
        return $this->execCommand("SADD", $member);
    }

    public function SCARD($key)
    {
        return $this->execCommand("SCARD", [$key]);
    }

    public function SISMEMBER($key, $member)
    {
        return $this->execCommand("SISMEMBER", [$key, $member]);
    }

    public function SMEMBERS($key)
    {
        return $this->execCommand("SMEMBERS", [$key]);
    }

    public function SREM($key, ...$members)
    {
        array_unshift($members, $key);
        return $this->execCommand("SREM", $members);
    }

    public function PING()
    {
        return $this->execCommand("PING", []);
    }

    //////////// 支持的命令列表 ///////////// }}}

    public function execCommand($cmd, $arguments)
    {
        if (empty($this->client)) {
            return null;
        }

        $tryTimes = 1;
        $ret = null;
        while ($tryTimes++ <= 2) {
            try {
                return $this->retry($cmd, $arguments);
            } catch (\RedisException $re) {
                // 处理读数据异常或链接断开，针对 php redis 扩展做的特殊逻辑
                $haveException = $re->getMessage();
                if (empty($haveException)) {
                    $haveException = "jaguar has gone away";
                }
                $this->errorLog([$cmd, $arguments], $re, $haveException);
                // 重新连接
                $this->reconnect();
            }
        }
        return $ret;
    }

    private function log($detail)
    {
        $logLevel = Log::currentLogLevel();
        $slow = Config::config('redis.slow.ms');
        $config = Config::getJaguar($this->name);
        if (isset($config['add_log']) && $config['add_log'] == 0) {
            return;
        }
        if (isset($detail['ms']) && $slow && $detail['ms'] >= $slow) {
            $tagsInflux = 'jaguar_slow_log' . ',service=' . CORE_NAME . ',uri=' . Request::uri();
            $fieldsInflux = ' cmd="' . $detail['cmd'] . '",ms=' . round(
                $detail['ms'],
                3
                ) . ',trace_id="' . Request::traceId() . '" ';
            $timeInflux = number_format(microtime(true) * 1000000000, 0, '', '');
            $afterInflux = "\n";
            Job::fire('log', 'jaguar_slow_log', $tagsInflux . $fieldsInflux . $timeInflux . $afterInflux, 'influx_');
            Log::redis_slow(LOG_WARNING, $detail);
        }
        if ($logLevel > LOG_INFO) {
            Log::redis(LOG_INFO, $detail);
            return;
        }
        $key = $detail['query'][1][0] ?? '';
        if ($key && isset($config['key_filter'])) {
            $filters = explode(',', $config['key_filter']);
            foreach ($filters as $filter) {
                if ($filter && substr($key, 0, strlen($filter)) === $filter) {
                    Log::redis_filter(LOG_INFO, $detail);
                    return;
                }
            }
        }
    }

    /**
     * 记录错误日志
     * @param $query
     * @param $ex
     */
    private function errorLog($query, $ex, $msg = '')
    {
        $detail = [
            'error_type' => 'jaguar',
            'error_source' => self::getPoolKeyByName($this->name),
            'query' => $query,
            'msg' => $msg,
        ];
        $context = [
            'error_code' => $ex->getCode(),
            'message' => $ex->getMessage(),
            'trace' => $ex->getTrace(),
        ];
        Log::error(LOG_ERR, $detail, $context);
    }
}
